/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.schemas;



import com.bff.gaia.unified.sdk.annotations.Experimental;

import com.bff.gaia.unified.sdk.annotations.Experimental.Kind;

import com.bff.gaia.unified.sdk.schemas.annotations.SchemaFieldName;

import com.bff.gaia.unified.sdk.schemas.annotations.SchemaIgnore;

import com.bff.gaia.unified.sdk.schemas.utils.FieldValueTypeSupplier;

import com.bff.gaia.unified.sdk.schemas.utils.JavaBeanUtils;

import com.bff.gaia.unified.sdk.schemas.utils.ReflectUtils;

import com.bff.gaia.unified.sdk.values.TypeDescriptor;

import com.bff.gaia.unified.vendor.guava.com.google.common.annotations.VisibleForTesting;



import java.lang.reflect.Constructor;

import java.lang.reflect.Method;

import java.util.List;

import java.util.stream.Collectors;



/**

 * A {@link SchemaProvider} for Java Bean objects.

 *

 * <p>This provider finds (recursively) all public getters and setters in a Java object, and creates

 * schemas and rows that bind to those fields. The field order in the schema is not guaranteed to

 * match the method order in the class. The Java object is expected to have implemented a correct

 * .equals() and .hashCode methods The equals method must be completely determined by the schema

 * fields. i.e. if the object has hidden fields that are not reflected in the schema but are

 * compared in equals, then results will be incorrect.

 *

 * <p>TODO: Validate equals() method is provided, and if not generate a "slow" equals method based

 * on the schema.

 */

@Experimental(Kind.SCHEMAS)

public class JavaBeanSchema extends GetterBasedSchemaProvider {

  /** {@link FieldValueTypeSupplier} that's based on getter methods. */

  @VisibleForTesting

  public static class GetterTypeSupplier implements FieldValueTypeSupplier {

    public static final GetterTypeSupplier INSTANCE = new GetterTypeSupplier();



    @Override

    public List<FieldValueTypeInformation> get(Class<?> clazz) {

      return ReflectUtils.getMethods(clazz).stream()

          .filter(ReflectUtils::isGetter)

          .filter(m -> !m.isAnnotationPresent(SchemaIgnore.class))

          .map(FieldValueTypeInformation::forGetter)

          .map(

              t -> {

                SchemaFieldName fieldName = t.getMethod().getAnnotation(SchemaFieldName.class);

                return (fieldName != null) ? t.withName(fieldName.value()) : t;

              })

          .collect(Collectors.toList());

    }

  }



  /** {@link FieldValueTypeSupplier} that's based on setter methods. */

  @VisibleForTesting

  public static class SetterTypeSupplier implements FieldValueTypeSupplier {

    private static final SetterTypeSupplier INSTANCE = new SetterTypeSupplier();



    @Override

    public List<FieldValueTypeInformation> get(Class<?> clazz) {

      return ReflectUtils.getMethods(clazz).stream()

          .filter(ReflectUtils::isSetter)

          .filter(m -> !m.isAnnotationPresent(SchemaIgnore.class))

          .map(FieldValueTypeInformation::forSetter)

          .collect(Collectors.toList());

    }

  }



  @Override

  public <T> Schema schemaFor(TypeDescriptor<T> typeDescriptor) {

    Schema schema =

        JavaBeanUtils.schemaFromJavaBeanClass(

            typeDescriptor.getRawType(), GetterTypeSupplier.INSTANCE);



    // If there are no creator methods, then validate that we have setters for every field.

    // Otherwise, we will have not way of creating the class.

    if (ReflectUtils.getAnnotatedCreateMethod(typeDescriptor.getRawType()) == null

        && ReflectUtils.getAnnotatedConstructor(typeDescriptor.getRawType()) == null) {

      JavaBeanUtils.validateJavaBean(

          GetterTypeSupplier.INSTANCE.get(typeDescriptor.getRawType(), schema),

          SetterTypeSupplier.INSTANCE.get(typeDescriptor.getRawType(), schema));

    }

    return schema;

  }



  @Override

  public FieldValueGetterFactory fieldValueGetterFactory() {

    return (Class<?> targetClass, Schema schema) ->

        JavaBeanUtils.getGetters(targetClass, schema, GetterTypeSupplier.INSTANCE);

  }



  @Override

  UserTypeCreatorFactory schemaTypeCreatorFactory() {

    UserTypeCreatorFactory setterBasedFactory =

        new SetterBasedCreatorFactory(new JavaBeanSetterFactory());



    return (Class<?> targetClass, Schema schema) -> {

      // If a static method is marked with @SchemaCreate, use that.

      Method annotated = ReflectUtils.getAnnotatedCreateMethod(targetClass);

      if (annotated != null) {

        return JavaBeanUtils.getStaticCreator(

            targetClass, annotated, schema, GetterTypeSupplier.INSTANCE);

      }



      // If a Constructor was tagged with @SchemaCreate, invoke that constructor.

      Constructor<?> constructor = ReflectUtils.getAnnotatedConstructor(targetClass);

      if (constructor != null) {

        return JavaBeanUtils.getConstructorCreator(

            targetClass, constructor, schema, GetterTypeSupplier.INSTANCE);

      }



      return setterBasedFactory.create(targetClass, schema);

    };

  }



  @Override

  public FieldValueTypeInformationFactory fieldValueTypeInformationFactory() {

    return (Class<?> targetClass, Schema schema) ->

        JavaBeanUtils.getFieldTypes(targetClass, schema, GetterTypeSupplier.INSTANCE);

  }



  /** A factory for creating {@link FieldValueSetter} objects for a JavaBean object. */

  public static class JavaBeanSetterFactory implements FieldValueSetterFactory {

    @Override

    public List<FieldValueSetter> create(Class<?> targetClass, Schema schema) {

      return JavaBeanUtils.getSetters(targetClass, schema, SetterTypeSupplier.INSTANCE);

    }

  }

}